/*******************************************************************************
 * Copyright (c) 2012 Till Kolditz.
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  which accompanies this distribution, and is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 * 
 * created on 15.10.2012 at 16:06:48
 * 
 *  Contributors:
 *      Till Kolditz
 *******************************************************************************/
package de.cc.yFilesParser.wizard;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.apache.log4j.Logger;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.operation.IRunnableWithProgress;

import de.kolditz.common.concurrent.Scheduler;
import de.kolditz.common.util.Pair;
import de.kolditz.common.util.SystemProperties;

/**
 * TODO several node types -> classes / attributes -> root nodes without attributes TODO nodes with multiple parents
 * TODO graphs without distinct roots TODO graphs containing circles
 * 
 * @author Till Kolditz - Till.Kolditz@GoogleMail.com
 */
public final class ConvertGraphmlToSMLRunnable extends GraphMLRunnable implements IRunnableWithProgress
{
    private Logger log = Logger.getLogger(getClass().getSimpleName());

    private Writer writer = null;
    private boolean isReverseLogic;
    private Collection<String> classShapes, attrShapes;

    public ConvertGraphmlToSMLRunnable(ConvertWizard wizard)
    {
        super(wizard);
    }

    @Override
    public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException
    {
        super.run(monitor);

        monitor.beginTask("Converting from GraphML to SML/NJ", IProgressMonitor.UNKNOWN);

        try
        {
            classShapes = wizard.settingsPage.getClassShapes();
            attrShapes = wizard.settingsPage.getAttributeShapes();
            isReverseLogic = wizard.settingsPage.isReverse();

            createReader(wizard.startPage.getSourceFile());
            createWriter(wizard.startPage.getTargetFile());

            monitor.subTask("Parsing source document");
            Map<String, Node> classes = new HashMap<String, Node>(); // maps nodeID -> node
            Map<String, Node> attributes = new HashMap<String, Node>(); // maps nodeID -> node (attribute of another
                                                                        // node)
            String nodegraphicID = null;
            while (reader.hasNext())
            {
                switch (reader.next())
                {
                    case XMLStreamReader.START_ELEMENT:
                        if (reader.hasName())
                        {
                            String nodeName = reader.getName().getLocalPart().trim();
                            if ("key".equals(nodeName))
                            {
                                Key key = parseKey("node", "nodegraphics");
                                if (key != null)
                                    nodegraphicID = key.attrId;
                            }
                            else
                                if ("node".equals(nodeName))
                                {
                                    Node node = parseNode(nodegraphicID);
                                    if (classShapes.contains(node.shape))
                                    {
                                        classes.put(node.id, node);
                                    }
                                    else
                                        if (attrShapes.contains(node.shape))
                                        {
                                            attributes.put(node.id, node);
                                        }
                                }
                                else
                                    if ("edge".equals(nodeName))
                                    {
                                        parseEdge(classes, attributes);
                                    }
                        }
                        break;
                }
                if (monitor.isCanceled())
                    return;
            }
            // search for all root nodes
            List<Node> rootNodes = new LinkedList<Node>();
            findRootNodes(classes, rootNodes);
            if (monitor.isCanceled())
                return;
            writeFile(rootNodes, classes);
            monitor.done();
        }
        catch (Exception e)
        {
            throw new InvocationTargetException(e);
        }
        finally
        {
            close();
        }
    }

    /**
     * @param nodes
     * @param rootNodes
     */
    private void findRootNodes(Map<String, Node> nodes, List<Node> rootNodes)
    {
        Node node;
        // check each entry in a depth-first-like attempt
        for (Entry<String, Node> entry : nodes.entrySet())
        {
            node = entry.getValue();
            while (node.wasChecked == false)
            {
                // abbreviation! will prevent from
                node.wasChecked = true;
                if (node.parent != null)
                    node = node.parent;
                else
                    rootNodes.add(node);
            }
        }
        resetChecked(nodes);
    }

    /**
     * @throws Exception
     */
    private void createWriter(final File targetFile) throws Exception
    {
        monitor.subTask("Opening target file for writing");

        if (!targetFile.exists() && !targetFile.createNewFile())
        {
            throw new IllegalStateException("Cannot create target file \"" + targetFile.getAbsolutePath() + "\"");
        }

        for (int i = 0; i < 5 && writer == null; ++i)
        {
            ScheduledFuture<Writer> future = Scheduler.schedule(new Callable<Writer>()
            {
                @Override
                public Writer call() throws Exception
                {
                    return new BufferedWriter(new FileWriter(targetFile));
                }
            }, 0, TimeUnit.MILLISECONDS);

            // wait for opening the file
            while (!future.isDone())
            {
                writer = future.get(100, TimeUnit.MILLISECONDS);
                if (monitor.isCanceled())
                {
                    future.cancel(true);
                    return;
                }
            }
        }

        if (writer == null)
        {
            throw new IllegalStateException(
                    "It seems that the target file could not be opened for writing, but no exception was caught");
        }
    }

    /**
     * @param classes
     *            the nodes which constitute classes
     * @param attributes
     *            the nodes which constitute attributes
     */
    private void parseEdge(Map<String, Node> classes, Map<String, Node> attributes) throws XMLStreamException
    {
        monitor.subTask("parsing \"edge\"");
        String id = null, source = null, target = null;

        int count = reader.getAttributeCount();
        for (int i = 0; i < count; ++i)
        {
            if ("id".equals(reader.getAttributeLocalName(i)))
            {
                id = reader.getAttributeValue(i);
            }
            else
                if ("source".equals(reader.getAttributeLocalName(i)))
                {
                    source = reader.getAttributeValue(i);
                }
                else
                    if ("target".equals(reader.getAttributeLocalName(i)))
                    {
                        target = reader.getAttributeValue(i);
                    }
        }

        String edgeText = findEdgeText();
        if (edgeText == null)
            edgeText = ""; // just make an empty attribute-key...

        Node s, t;
        if (id != null && source != null && target != null)
        {
            s = classes.get(source);
            if (s == null)
                s = attributes.get(source);
            t = classes.get(target);
            if (t == null)
                t = attributes.get(target);
            String msg = null;
            if (s == null)
            {
                msg = "No source node found for id " + source;
                openErrorDialog(new NullPointerException(msg));
                log.error(msg);
            }
            if (t == null)
            {
                msg = "No target node found for id " + target;
                openErrorDialog(new NullPointerException(msg));
                log.error(msg);
            }
            if (msg == null)
            {
                if (isReverseLogic)
                {
                    if (classes.containsKey(t.id))
                    {
                        s.addSubNode(t);
                    }
                    else
                        if (attributes.containsKey(t.id))
                        {
                            s.addAttribute(edgeText, t.name);
                        }
                        else
                        {
                            log.warn("node " + t.id + " [" + t.name + "] is neither class nor attribute");
                        }
                }
                else
                {
                    if (classes.containsKey(s.id))
                    {
                        t.addSubNode(s);
                    }
                    else
                        if (attributes.containsKey(s.id))
                        {
                            t.addAttribute(edgeText, s.name);
                        }
                        else
                        {
                            log.warn("node " + s.id + " [" + s.name + "] is neither class nor attribute");
                        }
                }
            }
        }
    }

    private String findEdgeText() throws XMLStreamException
    {
        boolean nodeEndNotFound = true;
        String edgeText = null;
        while (nodeEndNotFound && reader.hasNext())
        {
            switch (reader.next())
            {
                case XMLStreamReader.START_ELEMENT:
                    if (reader.hasName())
                    {
                        String nodeName = reader.getName().getLocalPart().trim();
                        if ("EdgeLabel".equals(nodeName))
                        {
                            boolean doContinue = true;
                            while (doContinue && reader.hasNext())
                            {
                                switch (reader.next())
                                {
                                    case XMLStreamConstants.START_ELEMENT:
                                        doContinue = false;
                                        break;
                                    case XMLStreamConstants.CHARACTERS:
                                        // takes the very first characters event as the node's name
                                        edgeText = reader.getText();
                                        doContinue = false;
                                        break;
                                    case XMLStreamConstants.END_ELEMENT:
                                        doContinue = false;
                                        break;
                                }
                            }
                        }
                    }
                    break;

                case XMLStreamConstants.END_ELEMENT:
                    if ("edge".equals(reader.getLocalName()))
                        nodeEndNotFound = false;
                    break;

                case XMLStreamConstants.END_DOCUMENT:
                    throw new IllegalStateException("premature end of document while parsing an edge node!");
            }
            if (monitor.isCanceled())
                return null;
        }
        return edgeText;
    }

    /**
     * @param rootNodes
     * @param nodes
     */
    private void writeFile(List<Node> rootNodes, Map<String, Node> nodes) throws IOException
    {
        monitor.subTask("Writing to target file");
        String varName = wizard.settingsPage.getVariableName();
        String parentName = wizard.settingsPage.getParentName();
        String childName = wizard.settingsPage.getChildName();
        Boolean pretty = wizard.settingsPage.isPrettyPrint();
        boolean pp = pretty != null ? pretty.booleanValue() : false;
        writer.append("val ").append(varName).append(" = [");
        if (pp)
            writer.append(SystemProperties.LINE_SEP);
        boolean writeSep = false;
        for (Node root : rootNodes)
        {
            root.wasChecked = true;
            if (writeSep)
                writer.append(',');
            else
                writeSep = true;
            writer.append("{").append(parentName).append("=\"\",").append(childName).append("=\"").append(root.name)
                    .append("\",a=[");
            writeAttributes(root);
            writer.append("]}");
            if (pp)
                writer.append(SystemProperties.LINE_SEP);
            writeSubNodes(root, parentName, childName, pp);
            if (monitor.isCanceled())
                return;
        }
        if (monitor.isCanceled())
            return;
        writer.append("]");
        resetChecked(nodes);
    }

    /**
     * @param node
     * @throws IOException
     */
    private void writeAttributes(Node node) throws IOException
    {
        if (node.hasAttributes())
        {
            boolean writeSep = false;
            for (Pair<String, String> p : node.attributes)
            {
                if (writeSep)
                    writer.append(',');
                else
                    writeSep = true;
                writer.append("k=\"").append(p.getKey()).append("\",v=\"").append(p.getValue()).append("\"");
            }
        }
    }

    /**
     * @param root
     * @param parentName
     * @param childName
     * @param pp
     *            pretty print?
     * @throws IOException
     */
    private void writeSubNodes(Node root, String parentName, String childName, boolean pp) throws IOException
    {
        if (monitor.isCanceled())
            return;
        if (root.hasSubNodes())
        {
            for (Node sub : root.subNodes)
            {
                if (!sub.wasChecked)
                {
                    writer.append(",{").append(parentName).append("=\"").append(root.name).append("\",")
                            .append(childName).append("=\"").append(sub.name).append("\",a=[");
                    writeAttributes(sub);
                    writer.append("]}");
                    if (pp)
                        writer.append(SystemProperties.LINE_SEP);
                    sub.wasChecked = true;
                    writeSubNodes(sub, parentName, childName, pp);
                }
            }
        }
    }

    /**
     * Not cancelable
     */
    protected void close()
    {
        super.close();
        monitor.subTask("Closing writer stream");
        ScheduledFuture<?> future = null;
        if (writer != null)
            future = Scheduler.schedule(new Runnable()
            {
                @Override
                public void run()
                {
                    try
                    {
                        writer.close();
                    }
                    catch (IOException e)
                    {
                        e.printStackTrace();
                    }
                }
            }, 0, TimeUnit.MILLISECONDS);

        // wait for opening the file
        while (!future.isDone())
        {
            try
            {
                future.get(100, TimeUnit.MILLISECONDS);
                if (monitor.isCanceled())
                {
                    future.cancel(true);
                    return;
                }
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }
}
